이번에는 컴퓨터 게임의 캐릭터를 만드는 예제를 통해 상속(Inheritance)의 개념을 공부한다.
컴퓨터 게임에 사용되는 플레이어의 캐릭터는 객체 지향 프로그램을 통해 만든다고 생각해 보자. 캐릭터의 능력치, 경험치 등의 숫자는 캐릭터마다 다르게 관리되어야 하므로 객체의 속성이 될 수 있다. 또한 모든 캐릭터 조작에 공통적으로 필요한 이동, 공격 등의 조작은 메서드로 구현할 수 있을 것이다.
이를 기반으로 캐릭터를 만들어내는 Character
라는 클래스를 만든다. 이 클래스로 만든 캐릭터는 1000 이라는 life 속성값을 가지고 생성되며 게임상에서 공격받을 경우에는 attacked
라는 메서드가 호출되어 life 속성값을 10만큼 감속시키고 공격 받았음을 표시한다.
In [1]:
class Character(object):
def __init__(self):
self.life = 1000
def attacked(self):
self.life -= 10
print(u"공격받음! 생명력 =", self.life)
이 클래스로 a
, b
, c
세 개의 캐릭터 객체를 생성한다.
In [2]:
a = Character()
b = Character()
c = Character()
모든 객체의 초기 life 속성값은 모두 1000이다.
In [3]:
a.life, b.life, c.life
Out[3]:
하지만 공격을 받은 캐릭터의 생명력은 감소된다.
In [4]:
a.attacked()
In [5]:
b.attacked()
In [6]:
a.attacked()
a.attacked()
a.attacked()
a.attacked()
a.attacked()
In [7]:
a.life, b.life, c.life
Out[7]:
이제 클래스 상속(class inheritance)이라는 개념을 생각한다. 위에서 만들어 본 클래스는 모든 캐릭터에 공통적인 life 속성만을 가지고 있었다. 하지만 만약 캐릭터도 전사(Warrior), 마법사(Wizard) 등 다양한 직업을 가진 캐릭터가 있고 각 캐릭터들은 서로 다른 초기 속성값을 가지고 태어난다면 어떻게 프로그램해야 할까? 각각의 직업 캐릭터를 별도의 클래스로 만들어도 되겠지만 클래스 상속을 사용하면 이미 만들어진 클래스 코드를 재사용하여 다른 클래스를 생성할 수 있다. 즉, 상속 과정에서 공통적으로 사용하는 속성이나 메서드는 두 번 반복해서 코딩할 필요가 없다. 이 때 상속을 받는 클래스를 자식 클래스(child class), 상속의 대상이 되는 클래스를 부모 클래스(parent class)라고 한다.
Character
부모 클래스에서 상속을 통해 Warrior
라는 자식 클래스와 Wizard
라는 자식 클래스를 만든다 상속을 위한 파이썬 문법은 다음과 같다.
class 자식클래스이름(부모클래스이름):
def __init__(self, 속성값1, 속성값2):
super(자식클래스이름, self).__init()
자식 클래스의 초기화 코드
사실 우리가 지금까지 쓰던 클래스 정의를 살펴보면 object
라는 부모 클래스에서 상속을 받는 것이었다.
이 코드에서 super(자식클래스이름, self).__init()
부분은 부모 클래스의 초기화 생성자를 호출하는 부분이다. 예를 들어 Warrior
라는 클래스에서 부모 클래스인 Character
클래스의 생성자를 호출하면 life라는 속성값을 초기화하므로 자식 클래스에서는 이 속성값을 초기화해줄 필요가 없다.
In [8]:
class Warrior(Character):
def __init__(self):
super(Warrior, self).__init__()
self.strength = 15
self.intelligence = 5
In [9]:
class Wizard(Character):
def __init__(self):
super(Wizard, self).__init__()
self.strength = 5
self.intelligence = 15
이 클래스의 객체를 만들어 보면 명시적으로 만들지 않았지만 life라는 속성과 attacked 라는 메서드를 가진다.
In [10]:
a = Warrior()
b = Wizard()
In [11]:
a.life, b.life
Out[11]:
In [12]:
a.strength, b.strength
Out[12]:
In [13]:
a.intelligence, b.intelligence
Out[13]:
In [14]:
a.attacked()
In [15]:
b.attacked()
메소드 오버라이딩(Method Overriding) 여러 클래스에 걸쳐서 같은 이름의 메서드를 만드는 것이다. 예를 들어 부모 클래스, 전사 캐릭터 클래스, 마법사 캐릭터 클래스에 공통적으로 attack
이라는 메서드가 있지만 각각의 하는 일이 다른 경우에는 다음과 같이 같은 이름의 메서드를 클래스 별로 구현하면 된다. 이렇게 되면 부모 클래스에서 만든 메서드 정의를 자식 클래스에서는 변경해서 사용한다.
In [16]:
class Character(object):
def __init__(self):
self.life = 1000
self.strength = 10
self.intelligence = 10
def attacked(self):
self.life -= 10
print(u"공격받음! 생명력 =", self.life)
def attack(self):
print(u"공격!")
In [17]:
class Warrior(Character):
def __init__(self):
super(Warrior, self).__init__()
self.strength = 15
self.intelligence = 5
def attack(self):
print(u"육탄 공격!")
In [18]:
class Wizard(Character):
def __init__(self):
super(Wizard, self).__init__()
self.strength = 5
self.intelligence = 15
def attack(self):
print(u"마법 공격!")
In [19]:
a = Character()
b = Warrior()
c = Wizard()
In [20]:
a.attack()
In [21]:
b.attack()
In [22]:
c.attack()
In [23]:
a.attacked()
In [24]:
b.attacked()
이와 비슷한 이름으로 오버로딩(Overloading)이라는 것이 있는데 이는 전혀 다른 개념이다. 오버로딩은 같은 메서드가 인수의 자료형이나 갯수를 다르게 받을 수 있는 것을 말한다. C++, Java 등에서는 지원하지만 파이썬에서는 오버로딩을 지원하지 않으므로 프로그래머가 내부적으로 알아서 처리해야 한다
다음은 C++에서 오버로딩을 지원하는 함수 선언의 예이다.
float length(list p1, list p2); // 점 (p1[0], p1[1]) - (p1[0], p1[1]) 까지의 길이
float length(int x1, int y1, int x2, int y2); // 점 (x1, y1) - (x2, y2) 까지의 길이
위의 게임 캐릭터 코드에서 attacked
메서드도 오버라이딩을 하여 전사와 마법사가 공격을 받을 때 life 속성값이 다르게 감소하도록 한다.
다음과 같이 자동차를 나타내는 Car
클래스를 구현한다.
max_speed
라는 속성과 현재 속도를 나타내는 speed
라는 속성을 가진다.max_speed
속성은 160이 되고 speed
속성은 0이 된다.speed_up
, speed_down
이라는 메서드를 가진다. speed_up
을 호출하면 speed
속성이 20씩 증가하고 speed_down
을 호출하면 speed
속성이 20씩 감소한다.speed
의 값은 max_speed
속성 값, 즉 160을 넘을 수 없다. 또 0 미만으로 감소할 수도 없다.Car
클래스를 기반으로 SportCar
와 Truck
이라는 두 개의 자식 클래스를 구현한다.
SportCar
클래스는 max_speed
속성이 200 이고 speed_up
, speed_down
호출시 속도가 40씩 증가 혹은 감소한다.Truck
클래스는 max_speed
속성이 100 이고 speed_up
, speed_down
호출시 속도가 10씩 증가 혹은 감소한다.speed
의 값은 max_speed
속성 값, 즉 160을 넘을 수 없다. 또 0 미만으로 감소할 수도 없다.파이썬은 들여쓰기를 통해 코드 블럭이 존재함을 알려주어야 한다. 그런데 내용이 없는 클래스나 함수의 경우에는 아무것도 쓰지 않는다면 코드 블럭의 존재 자체도 알려주기 못하기 때문에 pass
라는 아무 의미 없는 명령을 사용하여 들여쓰기된 코드 블럭을 표시한다.
In [25]:
def Dummy():
pass
d = Dummy()
파이썬에는 특수 메사드(Special Methods)라는 것이 존재한다. 메서드 이름의 앞과 뒤에 두 개의 밑줄(underscore)이 붙어있는 메서드이다. 이 메서드들은 특수한 용도에 사용하는 것이다
예를 들어 파이선 셸에서 변수 이름을 치고 엔터 키(주피터 노트북의 경우에는 시프트 + 엔트)를 치면 변수의 값이 호출되는데 사실 이것은 해당 변수가 가지는 __repr__
이라는 메서드가 호출되는 것이다. repr은 representation의 약자이다. 또 변수를 str
이라는 함수에 넣으면 변수를 문자열로 변환해 주는데 이것도 사실은 __str__
이라는 메서드가 호출되는 것이다.
예를 들어 다음과 같이 복소수에 대한 클래스인 Complex
클래스를 만든다. r
이라는 속성에 실수부를, i
이라는 속성에 실수부를 넣는다.
In [26]:
class Complex(object):
def __init__(self, realpart, imagpart):
self.r = realpart
self.i = imagpart
__repr__
메서드를 정의하지 않으면 object
클래스가 가진 기본 __repr__
메서드를 사용한다. 이 함수는 클래스 이름과 변수가 위치하고 있는 메모리 주소를 <>
안에 써서 반환한다. 기본 __str__
메서드도 마찬가지이다.
In [27]:
c = Complex(1, 2)
c
Out[27]:
In [28]:
str(c)
Out[28]:
이번에는 __repr__
메서드와 __repr__
메서드를 다음과 같이 새로 정의하여 오버라이딩한다.
In [29]:
class Complex2(Complex):
def __repr__(self):
return "Complex: real = %f imag = %f" % (self.r, self.i)
def __str__(self):
return "[for str] " + self.__repr__()
In [30]:
c2 = Complex2(1, 1)
c2
Out[30]:
In [31]:
str(c2)
Out[31]:
__getitem__
메서드를 정의하면 마치 리스트나 사전처럼 []
기호를 사용한 인덱싱을 할 수 있다.
In [32]:
class Complex3(Complex2):
def __getitem__(self, key):
if key == "r":
return self.r
if key == "i":
return self.i
In [33]:
c3 = Complex3(1, 2)
c3
Out[33]:
In [34]:
c3["i"]
Out[34]:
학생의 학번, 이름, 수학 성적, 영어 성적을 저장할 수 있는 클래스를 만들고 평균 성적을 출력하는 메서드를 추가한다. 셀에서 이 클래스 객체의 이름을 치면 바로 이름과 학번이 나와야 하고 str 명령을 수행하면 이름이 나와야 한다. 다음과 같이 수학과 영어 성적을 읽을 수 있어야 한다.
obj["math"], obj["english"]